JWT登出功能是一個關鍵的安全性考慮,因為JWT是無狀態的,一旦簽發,就無法撤銷或註銷。然而,有幾種方法可以實現JWT登出功能,每種方法都有其優點和限制。
工作原理:
工作原理:
如果你本地已搭建Redis服務可以跳過這一步驟
service:
  redis:
    container_name: 'myredis'
    image: redis:7.0-alpine
    restart: always
    ports:
      - "6380:6379" # 將容器內的6379映射到本機的6380埠
    environment:
      host: localhost #Redis伺服器位址
      port: 6379 #Redis伺服器埠號
      database: 0 #Redis資料庫索引
    networks:
      - backend
  # 省略其他服務
  
networks:
  backend:
首先,確保您的Spring Boot項目中包含了Redis的依賴項。可以在項目的pom.xml文件中添加以下依賴項:
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在application.yml中配置Redis連接信息。這包括Redis伺服器的主機和和埠號:
application.yml:application-dev.yml,這樣的好處是當換成生產環境只要改成spring.profiles.active: prod,並準備好application-prod.yml生產環境配置即可,詳細教學可以參考此系列的第6天文章。spring:
  profiles:
    active: dev # 導入application-dev.yml開發環境配置
---
spring:
  data:
    redis:
      host: ${conf.redis.host}  #Redis伺服器位址 (預設 localhost)
      port: ${conf.redis.port} #Redis伺服器埠號 (預設 6379)
      database: ${conf.redis.database}  #Redis資料庫索引 (預設 0)
      timeout: 4000 # 讀取超時
application-dev.yml:conf:
  token:
    secret: "your secret key"
    expiration: 900000 #Token有效期限 (設定15分鐘過期=15*60*1000 單位:毫秒)
  redis:
    host: localhost #Redis伺服器位址
    port: 6380 #Redis伺服器埠號
    database: 0 #Redis資料庫索引
為了實現黑名單登出功能,我們需要在Redis中維護一個令牌黑名單。可以使用Redis的String數據結構來實現,並且該值可以根據本身的存活時間自動被Redis清除。創建一個名為JwtBlackListService,用於處理JWT黑名單等相關業務處理服務。
@Service
@RequiredArgsConstructor
public class JwtBlackListService {
    @Value("${conf.token.expiration}")
    private long jwtExpiration;
    private final RedisTemplate<String, String> redisTemplate;
    private static final String BLACKLIST_PREFIX = "jwt:blacklist:"; // 黑名單前綴
    /** 將jwt加入黑名單 */
    public void addJwtToBlackList(String jwt) {
        String key = BLACKLIST_PREFIX + jwt;
        Duration expirationDuration  = Duration.ofMillis(jwtExpiration); // 設置存活時間
        // 將jwt加入黑名單並設置存活時間,當過了存活時間就會自動被Redis自動清除
        redisTemplate.opsForValue().set(key, "true", expirationDuration); 
    }
    /** 檢查jwt是否在黑名單中 */
    public boolean isJwtInBlackList(String jwt) {
        String key = BLACKLIST_PREFIX + jwt;
        return redisTemplate.opsForValue().get(key) != null;
    }
}
AuthenticationController.class
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthenticationController {
    
    private final AuthenticationService service;
    
    //** 登出接口 */
    @PostMapping("/logout")
    public ResponseEntity<StatusResponse> logout(@RequestHeader("Authorization") String token) {
        return ResponseEntity.ok(service.logout(token));
    }
    
    // 其他接口程式碼
}
AuthenticationService.class
@Service
@Slf4j
@AllArgsConstructor
public class AuthenticationService {
    
    private final JwtService jwtService;
    private final JwtBlackListService jwtBlackListService;    
    /** 使用者登出處理 */
    public StatusResponse logout(String token) {
        // 調用JWT黑名單服務將該token加入到黑名單中      
        jwtBlackListService.addJwtToBlackList(token.substring(7));
        // 清除Spring Security上下文
        SecurityContextHolder.clearContext();
        return StatusResponse.SUCCESS();
    }
    
    // 其他程式碼   
}
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;
    @Override
    protected void doFilterInternal(
        @NonNull HttpServletRequest request,
        @NonNull HttpServletResponse response,
        @NonNull FilterChain filterChain
    ) throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");
        final String jwt;
        final String userEmail;
        // 以下條件為沒有攜帶Token的請求
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        try {
            jwt = authHeader.substring(7); //取"Bearer "後面的Token
            userEmail = jwtService.extractUsername(jwt);
            if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
                //檢查token是否有效
                if (jwtService.isTokenValid(jwt, userDetails)) { // <---關注點
                    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                            userDetails,
                            null,
                            userDetails.getAuthorities()
                    );
                    authToken.setDetails(
                            new WebAuthenticationDetailsSource().buildDetails(request)
                    );
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                } else {
                    sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED, "Token失效,請重新申請");
                    return;
                }}
        } catch (Exception e) {
            // 異常處理的程式碼
        }
        filterChain.doFilter(request, response);
    }
}
@Service
@Slf4j
@RequiredArgsConstructor
public class JwtService {
    // Token有效期限
    @Value("${conf.token.expiration:900000}")
    private Long EXPIRATION_TIME; //單位ms
    @Value("${conf.token.secret}")
    private String SECRET_KEY;
    private final JwtBlackListService jwtBlackListService;
    
    /**
     * 驗證Token有效性,比對JWT和UserDetails的Username(Email)是否相同
     * @return 有效為True,反之False
     */
    public boolean isTokenValid(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()))
                && !isTokenExpired(token)
                && !isTokenInBlackList(token); // 多了這個是否在黑名單的判斷
    }
    /**
     * check if the JWT is in the blacklist
     * @return in the blacklist return true, else return false
     */
    private boolean isTokenInBlackList(String jwt) {
        return jwtBlackListService.isJwtInBlackList(jwt);
    }
    // 其他程式碼...
}
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtAuthenticationFilter jwtAuthFilter;
    private final AuthenticationProvider authenticationProvider;
    /**
     # 用戶認證配置 #
     1. authorizeHttpRequests()方法:對所有訪問HTTP端點的HttpServletRequest進行限制
     2. anyRequest().authenticated()語句指定了對於所有請求都需要執行認證,也就是說沒有通過認證的用戶就無法訪問任何端點。
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests((authorize) -> authorize
            .requestMatchers(
                    "/error/**",
                    "/api/smarthome/auth/register",           //用戶註冊
                    "/api/smarthome/auth/login",              //用戶登入
                    "/api/smarthome/auth/password/forgot",    //忘記密碼
                    "/api/smarthome/auth/verification/check", //檢查驗證碼
                    "/v3/api-docs/**",
                    "/swagger-ui/**",
                    "/api/test/**"
                    )
                    .permitAll()
                    .anyRequest()
                    .authenticated()
                    )
            .sessionManagement((session) -> session
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authenticationProvider(authenticationProvider)
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}
 



TTL "jwt:blacklist:{你要查詢的JWT}"

